/*!	 trgt_gif.cpp
**	 BMP Target Module
**
**	Copyright (c) 2002-2005 Robert B. Quattlebaum Jr., Adrian Bentley
**	Copyright (c) 2007 Chris Moore
**
**	This package is free software; you can redistribute it and/or
**	modify it under the terms of the GNU General Public License as
**	published by the Free Software Foundation; either version 2 of
**	the License, or (at your option) any later version.
**
**	This package is distributed in the hope that it will be useful,
**	but WITHOUT ANY WARRANTY; without even the implied warranty of
**	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
**	General Public License for more details.
**
** === N O T E S ===========================================================
**
** ========================================================================= */

#ifdef USING_PCH
#	include "pch.h"
#else
#ifdef HAVE_CONFIG_H
#	include <config.h>
#endif

#include <synfig/localization.h>
#include <synfig/general.h>

#include <ETL/stringf>
#include "trgt_gif.h"
#include <cstdio>
#endif

using namespace synfig;
using namespace std;
using namespace etl;

#define MAX_FRAME_RATE	(20.0)

SYNFIG_TARGET_INIT(gif);
SYNFIG_TARGET_SET_NAME(gif, "gif");
SYNFIG_TARGET_SET_EXT(gif, "gif");
SYNFIG_TARGET_SET_VERSION(gif, "0.1");
SYNFIG_TARGET_SET_CVS_ID(gif, "$Id$");

gif::gif(const char *filename_, const synfig::TargetParam & /* params */):
    bs(),
    filename(filename_),
    file((filename == "-") ? stdout : fopen(filename_, POPEN_BINARY_WRITE_TYPE)),
    i(),
    codesize(),
    rootsize(),
    nextcode(),
    table(NULL),
    next(NULL),
    node(NULL),
    imagecount(0),
    cur_scanline(),
    lossy(true),
    multi_image(false),
    dithering(true),
    color_bits(8),
    iframe_density(30),
    loop_count(0x7fff),
    local_palette(true)
{ }

gif::~gif()
{
    if (file) {
        fputc(';', file.get());    // Image terminator
    }
}

bool
gif::set_rend_desc(RendDesc *given_desc)
{
    if (given_desc->get_frame_rate() > MAX_FRAME_RATE) {
        given_desc->set_frame_rate(MAX_FRAME_RATE);
    }

    desc = *given_desc;

    if (desc.get_frame_end() - desc.get_frame_start() > 0) {
        multi_image = true;
        imagecount = desc.get_frame_end() - desc.get_frame_start();
    } else {
        multi_image = false;
    }

    return true;
}

bool
gif::init(synfig::ProgressCallback * /* cb */)
{
    int w = desc.get_w(), h = desc.get_h();

    if (!file) {
        synfig::error(strprintf(_("Unable to open \"%s\" for write access!"), filename.c_str()));
        return false;
    }

    rootsize = color_bits;	// Size of pixel bits

    curr_frame.set_wh(w, h);
    prev_frame.set_wh(w, h);
    curr_surface.set_wh(w, h);
    curr_frame.clear();
    prev_frame.clear();
    curr_surface.clear();

    if (get_quality() > 5) {
        lossy = true;
    } else {
        lossy = false;
    }

    // Output the header
    fprintf(file.get(), "GIF89a");
    fputc(w & 0x000000ff, file.get());
    fputc((w & 0x0000ff00) >> 8, file.get());
    fputc(h & 0x000000ff, file.get());
    fputc((h & 0x0000ff00) >> 8, file.get());

    if (!local_palette) {
        fputc(0xF0 + (rootsize - 1), file.get());    // flags
    } else {
        fputc((0xF0 + (rootsize - 1)) & ~(1 << 7), file.get());    // flags
    }

    fputc(0, file.get());		// background color
    fputc(0, file.get());		// Pixel Aspect Ratio

    if (!local_palette) {
        curr_palette = Palette::grayscale(256 / (1 << (8 - rootsize)) - 1);
        output_curr_palette();
    }

    if (loop_count && multi_image) {
        fputc(33, file.get()); // 33 (hex 0x21) GIF Extension code
        fputc(255, file.get()); // 255 (hex 0xFF) Application Extension Label
        fputc(11, file.get()); // 11 (hex (0x0B) Length of Application Block
        fprintf(file.get(), "NETSCAPE2.0");
        fputc(3, file.get()); // 3 (hex 0x03) Length of Data Sub-Block
        fputc(1, file.get());
        fputc(loop_count & 0x000000ff, file.get());
        fputc((loop_count & 0x0000ff00) >> 8, file.get());
        fputc(0, file.get()); // 0 (hex 0x00) a Data Sub-block Terminator.
    }

    return true;
}

void
gif::output_curr_palette()
{
    // Output the color table
    for (i = 0; i < 256 / (1 << (8 - rootsize)); i++) {
        if (i < (signed)curr_palette.size()) {
            Color color(curr_palette[i].color.clamped());
            fputc(gamma().r_F32_to_U8(color.get_r()), file.get());
            fputc(gamma().g_F32_to_U8(color.get_g()), file.get());
            fputc(gamma().b_F32_to_U8(color.get_b()), file.get());
        } else {
            fputc(255, file.get());
            fputc(0, file.get());
            fputc(255, file.get());
        }
    }
}

bool
gif::start_frame(synfig::ProgressCallback *callback)
{
//	int

    if (!file) {
        if (callback) {
            callback->error(string("BUG:") + _("Description not set!"));
        }

        return false;
    }

    if (callback) {
        callback->task(filename + strprintf(" %d", imagecount));
    }

    return true;
}

void
gif::end_frame()
{
    int w = desc.get_w(), h = desc.get_h(), i;
    unsigned int value;
    int
    delaytime = round_to_int(100.0 / desc.get_frame_rate());

    bool build_off_previous(multi_image);

    Palette prev_palette(curr_palette);

    // Fill in the background color
    if (get_alpha_mode() == TARGET_ALPHA_MODE_KEEP) {
        Surface::alpha_pen pen(curr_surface.begin(), 1.0, Color::BLEND_BEHIND);
        pen.set_value(get_canvas()->rend_desc().get_bg_color());

        for (int y = 0; y < curr_surface.get_h(); y++, pen.inc_y()) {
            int x;

            for (x = 0; x < curr_surface.get_w(); x++, pen.inc_x()) {
                if (pen.get_value().get_a() > 0.1) {
                    pen.put_value();
                } else {
                    pen[0][0] = Color::alpha();
                }
            }

            pen.dec_x(x);
        }
    }

    if (local_palette) {
        curr_palette = Palette(curr_surface, 256 / (1 << (8 - rootsize)) - build_off_previous - 1);
        synfig::info("curr_palette.size()=%d", curr_palette.size());
    }

    int transparent_index(curr_palette.find_closest(Color(1, 0, 1, 0)) - curr_palette.begin());
    bool has_transparency(curr_palette[transparent_index].color.get_a() <= 0.00001);

    if (has_transparency) {
        build_off_previous = false;
    }

    if (build_off_previous) {
        transparent_index = 0;
        has_transparency = true;
    }

#define DISPOSE_UNDEFINED			(0)
#define DISPOSE_NONE				(1<<2)
#define DISPOSE_RESTORE_BGCOLOR		(2<<2)
#define DISPOSE_RESTORE_PREVIOUS	(3<<2)
    int gec_flags(0);

    if (build_off_previous) {
        gec_flags |= DISPOSE_NONE;
    } else {
        gec_flags |= DISPOSE_RESTORE_PREVIOUS;
    }

    if (has_transparency) {
        gec_flags |= 1;
    }

    // output the Graphic Control Extension
    fputc(0x21, file.get()); // Extension introducer
    fputc(0xF9, file.get()); // Graphic Control Label
    fputc(4, file.get()); // Block Size
    fputc(gec_flags, file.get()); // Flags (Packed Fields)
    fputc(delaytime & 0x000000ff, file.get()); // Delay Time (MSB)
    fputc((delaytime & 0x0000ff00) >> 8, file.get()); // Delay Time (LSB)
    fputc(transparent_index, file.get()); // Transparent Color Index
    fputc(0, file.get()); // Block Terminator

    // output the image header
    fputc(',', file.get());
    fputc(0, file.get());	// image left
    fputc(0, file.get());	// image left
    fputc(0, file.get());	// image top
    fputc(0, file.get());	// image top
    fputc(w & 0x000000ff, file.get());
    fputc((w & 0x0000ff00) >> 8, file.get());
    fputc(h & 0x000000ff, file.get());
    fputc((h & 0x0000ff00) >> 8, file.get());

    if (local_palette) {
        fputc(0x80 | (rootsize - 1), file.get());    // flags
    } else {
        fputc(0x00 + rootsize - 1, file.get());    // flags
    }

    if (local_palette) {
        Palette out(curr_palette);

        if (build_off_previous) {
            curr_palette.insert(curr_palette.begin(), Color(1, 0, 1, 0));
        }

        output_curr_palette();
        curr_palette = out;
    }

    bs = bitstream(file);

    // Prepare ourselves for LZW compression
    codesize = rootsize + 1;
    nextcode = (1 << rootsize) + 2;
    table = lzwcode::NewTable((1 << rootsize));
    node = table;

    // Output the rootsize
    fputc(rootsize, file.get());
    // Push a table reset into the bitstream
    bs.push_value(1 << rootsize, codesize);

    for (int cur_scanline = 0; cur_scanline < desc.get_h(); cur_scanline++) {

        // Now we compress it!
        for (i = 0; i < w; i++) {
            Color color(curr_surface[cur_scanline][i].clamped());
            Palette::iterator iter(curr_palette.find_closest(color));

            if (dithering) {
                Color error(color - iter->color);

                if (curr_surface.get_h() > cur_scanline + 1) {
                    curr_surface[cur_scanline + 1][i - 1]  += error * ((float)3 / (float)16);
                    curr_surface[cur_scanline + 1][i]    += error * ((float)5 / (float)16);

                    if (curr_surface.get_w() > i + 1) {
                        curr_surface[cur_scanline + 1][i + 1]  += error * ((float)1 / (float)16);
                    }
                }

                if (curr_surface.get_w() > i + 1) {
                    curr_surface[cur_scanline][i + 1]    += error * ((float)7 / (float)16);
                }
            }

            curr_frame[cur_scanline][i] = iter - curr_palette.begin();

            value = curr_frame[cur_scanline][i];

            if (build_off_previous) {
                value++;
            }

            if (value > (unsigned)(1 << rootsize) - 1) {
                value = (1 << rootsize) - 1;
            }

            // If the pixel is the same as the one that
            // is already there, then we should make it
            // transparent
            if (build_off_previous) {
                if (lossy) {

                    // Lossy
                    if (
                        abs((iter->color - prev_palette[prev_frame[cur_scanline][i] - 1].color).get_y()) > (1.0 / 16.0) ||
                        (imagecount % iframe_density) == 0 || imagecount == desc.get_frame_end() - 1) { // lossy version
                        prev_frame[cur_scanline][i] = value;
                    } else {
                        prev_frame[cur_scanline][i] = value;
                        value = 0;
                    }
                } else {
                    // lossless version
                    if (value != prev_frame[cur_scanline][i]) {
                        prev_frame[cur_scanline][i] = value;
                    } else {
                        value = 0;
                    }
                }
            } else {
                prev_frame[cur_scanline][i] = value;
            }

            next = node->FindCode(value);

            if (next) {
                node = next;
            } else {
                node->AddNode(nextcode, value);
                bs.push_value(node->code, codesize);
                node = table->FindCode(value);

                // Check to see if we need to increase the codesize
                if (nextcode == (1 << codesize)) {
                    codesize += 1;
                }

                nextcode += 1;

                // check to see if we have filled up the table
                if (nextcode == 4096) {
                    // output the clear code: make sure to use the current
                    // codesize
                    bs.push_value((unsigned) 1 << rootsize, codesize);

                    delete table;
                    table = lzwcode::NewTable((1 << rootsize));
                    codesize = rootsize + 1;
                    nextcode = (1 << rootsize) + 2;

                    // since we have a new table, need the correct prefix
                    node = table->FindCode(value);
                }
            }
        }
    }

    // Push the last code onto the bitstream
    bs.push_value(node->code, codesize);

    // Push a end-of-stream code onto the bitstream
    bs.push_value((1 << rootsize) + 1, codesize);

    // Make sure everything is dumped out
    bs.dump();

    delete table;

    fputc(0, file.get());		// Block terminator

    fflush(file.get());
    imagecount++;
}

synfig::Color*
gif::start_scanline(int scanline)
{
    cur_scanline = scanline;
    return curr_surface[scanline];
}

bool
gif::end_scanline()
{
    if (!file) {
        return false;
    }

//	unsigned int value;

    return true;
}